#!/usr/bin/python

# Copyright 2011 Google Inc. All Rights Reserved.

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

#     http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


"""Checks a zone file (or set of zonefiles) on a slave to warn of expiration.

This reads a zonefile (or set of zonefiles) on a slave / secondary nameserver
and will warn if they are about to expire, have expired, etc.

BIND resets the mtime on the zonefile on successful AXFR / IXFR. If this time is
close to the expire time then something bad happened!
"""

import glob
import os
import sys
import time

try:
  import dns.zone
  from dns.exception import DNSException
except ImportError:
  print """
You need to install "dnspython". Try "apt-get install python-dnspython" on an
Ubuntu type machine, or go to "http://www.dnspython.org/". """
  sys.exit (-1)

from optparse import OptionParser
opts = False

# Constants.
ZONE_ERROR   = 4
ZONE_EXPIRED = 3
ZONE_RETRY   = 2
ZONE_REFRESH = 1
ZONE_HEALTHY = 0


def ParseOptions():
  """Parses the command line options."""
  global opts
  options = OptionParser()
  options.add_option('-v', '--verbose', dest='verbose',
                     action='store_true',
                     default=False,
                     help='Be more verbose in output.')
  options.add_option('-r', '--report', dest='report',
                     action='store_true',
                     default=False,
                     help='Print a little report at the end.')
  options.add_option('-d', '--directory', dest='dir',
                     default='/etc/namedb/slave/',
                     help='Where are the slave zones stored?.')
  options.add_option('-j', '--jitter', dest='jitter',
                     default='10',
                     help=('How many seconds slack to add -- this will make'
                            'allowances for transfers, skew, etc.'))
  options.add_option('-f', '--files', dest='files',
                     default='*',
                     help='Wildcard specifying the zones file to check.')

  (opts, args) = options.parse_args()
  return opts


def CheckZoneExpire(zone_file):
  """This checks a zone file to see when the expire time is and
     when the last time a successful zone transfer happened.

     This currently assumes that a file called '/foo/bar/example.com'
     is for the 'example.com' zone.
     
     Args:
       zone: A string containing a filename for a zone file.

     Returns:
       True if the zone is still healthy, else false.
  """
  
  opts.jitter = int(opts.jitter)
  try:
    zone_name = os.path.basename(zone_file)
    zone = dns.zone.from_file(zone_file, zone_name)
    soa = zone.get_rdataset('@', 'SOA')[0]

    file_age = int((time.time() - os.path.getmtime(zone_file)))
    if opts.verbose:
      print '\nWorking on: %s' % zone_file
      print '\t%s SOA: [%s]. ' % (zone_name,
                                  soa.to_text())
      print '\tLast transferred %s seconds ago.' % file_age

    if file_age > soa.expire + opts.jitter:
      print 'ERROR: %s has expired! (%s > (%s + %s))' % (zone_name, file_age,
                                                         soa.expire,
                                                         opts.jitter)
      return (ZONE_EXPIRED, zone_name)
    elif file_age > soa.refresh + soa.retry + opts.jitter:
      print 'NOTICE: %s failed more then once to transfer (%s > (%s + %s)' % (
        zone_name, file_age, soa.refresh, opts.jitter)
      return (ZONE_RETRY, zone_name)
    elif file_age > soa.refresh + opts.jitter:
      print 'INFO: %s failed to refresh (%s > (%s + %s))' % (
        zone_name, file_age, soa.refresh, opts.jitter)
      return (ZONE_REFRESH, zone_name)
    elif opts.verbose:
      print 'DEBUG: %s still healthy! (%s < (%s + %s)' % (
        zone_name, file_age, soa.refresh, opts.jitter)
    return (ZONE_HEALTHY, zone_name)
      
  except DNSException, e:
    print 'Unable to parse %s: %s' % (zone_file, e)
  except IOError, e:
    print 'Unable to access %s: %s' % (zone_file, e)
  return (ZONE_ERROR, zone_name)


def main():
  """Main."""

  worst = 0
  status = [[], [], [], [], [], []]
  global opts
  opts = ParseOptions()
  if not os.path.exists(opts.dir):
    print ('The specified directory (%s) for slave zones is missing,'
           ' or I do not have permissions to read it.' % 
           opts.dir)
    sys.exit(-1)

  files = glob.glob(os.path.join(opts.dir, opts.files))
  if not files:
    print 'Could not find any files in %s. Exiting...' % (
      os.path.join(opts.dir, opts.files))
    sys.exit(-1)
  if opts.verbose:
    print ('List of zone files:\n\t%s\n\n' % 
           ', '.join([zone_file for zone_file in files]))

  for zone_file in files:
    (zone_status, zone) = CheckZoneExpire(zone_file)
    status[zone_status].append(zone)
    worst = max(worst, zone_status)

  if opts.report:
    print '\n\n'
    print '-'*80
    print ('%s Errors:\t%s' % (
        len(status[ZONE_ERROR]),
        ', '.join([zone for zone in status[ZONE_ERROR]])))
    print ('%s Expire:\t%s' % (
        len(status[ZONE_EXPIRED]),
        ', '.join([zone for zone in status[ZONE_EXPIRED]])))
    print ('%s Retry:\t%s' % (
        len(status[ZONE_RETRY]),
        ', '.join([zone for zone in status[ZONE_RETRY]])))
    print ('%s Refresh:\t%s' % (
        len(status[ZONE_REFRESH]),
        ', '.join([zone for zone in status[ZONE_REFRESH]])))
    print ('%s Healthy:\t%s' % (
          len(status[ZONE_HEALTHY]),
          ', '.join([zone for zone in status[ZONE_HEALTHY]])))
    print '-'*80

  sys.exit(worst)

if __name__ == '__main__':
  main()
 
